<template>
  <span ref="countRef">{{ displayValue }}</span>
</template>

<script setup>
import { ref, onMounted, watch, onBeforeUnmount } from 'vue'

const props = defineProps({
  startVal: {
    type: Number,
    default: 0
  },
  endVal: {
    type: Number,
    required: true
  },
  duration: {
    type: Number,
    default: 2000
  },
  decimals: {
    type: Number,
    default: 0
  },
  prefix: {
    type: String,
    default: ''
  },
  suffix: {
    type: String,
    default: ''
  },
  separator: {
    type: String,
    default: ','
  },
  decimal: {
    type: String,
    default: '.'
  },
  useEasing: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['on-commenced', 'on-complete'])

const countRef = ref(null)
const displayValue = ref(props.startVal)
const frameId = ref(null)

// 缓动函数
const easeOutExpo = (t) => {
  return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
}

const formatNumber = (num) => {
  num = num.toFixed(props.decimals)
  const parts = num.toString().split('.')
  parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, props.separator)
  return parts.join(props.decimal)
}

const cancelAnimation = () => {
  if (frameId.value) {
    cancelAnimationFrame(frameId.value)
    frameId.value = null
  }
}

const startAnimation = (timestamp) => {
  if (!frameId.value) {
    return
  }

  if (!startTimestamp.value) {
    startTimestamp.value = timestamp
    emit('on-commenced')
  }

  const progress = timestamp - startTimestamp.value
  const remaining = Math.max(0, props.duration - progress)

  let t = progress / props.duration
  t = props.useEasing ? easeOutExpo(t) : t
  t = t > 1 ? 1 : t

  const currentValue = props.startVal + t * (props.endVal - props.startVal)
  displayValue.value = props.prefix + formatNumber(currentValue) + props.suffix

  if (progress < props.duration) {
    frameId.value = requestAnimationFrame(startAnimation)
  } else {
    cancelAnimation()
    emit('on-complete')
  }
}

const startTimestamp = ref(null)

const animateValue = () => {
  cancelAnimation()
  displayValue.value = formatNumber(props.startVal)
  startTimestamp.value = null
  frameId.value = requestAnimationFrame(startAnimation)
}

onMounted(() => {
  animateValue()
})

watch(() => props.endVal, (newVal, oldVal) => {
  props.startVal = displayValue.value
  animateValue()
})

onBeforeUnmount(() => {
  cancelAnimation()
})
</script>

<style scoped>
span {
  font-weight: bold;
}
</style>